import { ScrollBar } from "@/ScrollBar";
import hljs from 'highlight.js/lib/core';
import c from 'highlight.js/lib/languages/c';
import cpp from 'highlight.js/lib/languages/cpp';
import java from 'highlight.js/lib/languages/java';
import javascript from 'highlight.js/lib/languages/javascript';
import kotlin from 'highlight.js/lib/languages/kotlin';
import python from 'highlight.js/lib/languages/python';
import typescript from 'highlight.js/lib/languages/typescript';
import 'highlight.js/styles/github.min.css';
import styles from './style.module.less';
import { HighlightResult } from "highlight.js";

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('python', python);
hljs.registerLanguage('kotlin', kotlin);
hljs.registerLanguage('java', java);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('c', c);

export class CodeViewer {

  readonly scroll_bar: ScrollBar | null = null;
  protected first_pointer_id?: number
  protected lines_range: Range = document.createRange();
  protected line_num_1: Element | null = null;

  readonly ele_root: HTMLElement = document.createElement('div');
  readonly ele_head: HTMLDivElement = document.createElement('div');
  readonly ele_main: HTMLDivElement = document.createElement('div');
  readonly ele_lines: HTMLDivElement = document.createElement('div');
  readonly ele_pre: HTMLElement;
  readonly ele_code: HTMLElement;
  readonly data: HighlightResult;

  constructor(ele_pre: HTMLElement) {
    this.ele_pre = ele_pre;
    this.ele_pre.className = styles.pre
    this.ele_root.classList.add(styles.code_view)
    this.ele_main.classList.add(styles.main)
    this.ele_code = ele_pre.querySelector('code') || document.createElement('CODE');
    this.init_header();

    let language: string | undefined;
    for (const c of this.ele_code.classList) {
      language = c.match(/^language-(.*)/)?.[1];
      if (language) break;
    }
    this.data = hljs.highlight(this.ele_code.innerText, { language: language || 'javascript' })
    this.ele_code.innerHTML = this.data.value;
    this.init_lines();
    this.scroll_bar = new ScrollBar({
      target: this.ele_pre,
      direction: 'horizontal',
      container: this.ele_root,
      styles: {
        scroll_bar: {
          horizontal: {
            right: '0px',
            left: 'auto',
          }
        }
      }
    });
    this.ele_pre?.parentElement?.replaceChild(this.ele_root, this.ele_pre)
    this.ele_root.appendChild(this.ele_main);
    this.ele_main.appendChild(this.ele_pre);
  }

  protected _lines_info = new Map<Element, [number, Node, number, Node, number]>();

  protected init_header() {
    const { ele_code } = this;
    if (!ele_code) return;
    this.ele_head.classList.add(styles.header)
    this.ele_root?.appendChild(this.ele_head)
    return;
  }

  protected init_lines() {
    this.ele_lines.classList.add(styles.lines);
    this.ele_lines.addEventListener('pointerdown', this.on_lines_pointerdown)
    let line_number = 0;
    const children = Array.from(this.ele_code.childNodes);

    let node = children[0];
    let offset = 0
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      switch (child.nodeType) {
        case Node.ELEMENT_NODE:
          children.splice(i + 1, 0, ...Array.from(child.childNodes))
          break;
        case Node.TEXT_NODE:
          if (!child.textContent) break;
          let j = child.textContent.indexOf('\n');
          while (j >= 0) {
            const ele_line = document.createElement('div');
            ele_line.classList.add(styles.line);
            ele_line.innerHTML = `${++line_number}`;
            this.ele_lines.appendChild(ele_line);
            this._lines_info.set(ele_line, [line_number, node, offset, node = child, offset = j + 1])
            j = child.textContent.indexOf('\n', offset)
          }
          break;
        default:
          debugger
      }
    }
    this.ele_main.appendChild(this.ele_lines)
  }

  on_lines_pointerdown = (e: PointerEvent) => {
    if (e.button !== 0) return;
    if (this.first_pointer_id !== void 0) return;
    this.first_pointer_id = e.pointerId;
    const ele = this.line_num_1 = (e.target as Element);
    this.select_lines(ele, ele)
    document.addEventListener('pointermove', this.on_lines_pointermove)
    document.addEventListener('pointerup', this.on_lines_pointerup)
  }
  on_lines_pointermove = (e: PointerEvent) => {
    let ele = (e.target as Element);
    if (!this._lines_info.has(ele)) {
      for (const line of this._lines_info.keys()) {
        const { y, height } = line.getBoundingClientRect();
        if (e.clientY > y && e.clientY <= y + height) {
          ele = line;
          break;
        }
      }
    }
    this.select_lines(this.line_num_1!, ele);
  }
  on_lines_pointerup = (e: PointerEvent) => {
    if (this.first_pointer_id !== e.pointerId) return;
    document.removeEventListener('pointermove', this.on_lines_pointermove)
    document.removeEventListener('pointerup', this.on_lines_pointerup)
    delete this.first_pointer_id;
  }
  select_lines(line_1: Element, line_2: Element) {
    const info_0 = this._lines_info.get(line_1);
    const info_1 = this._lines_info.get(line_2);
    if (!info_0 || !info_1) return;
    let node_0: Node;
    let offset_0: number;
    let node_1: Node;
    let offset_1: number;
    if (info_0[0] <= info_1[0]) {
      node_0 = info_0[1];
      offset_0 = info_0[2];
      node_1 = info_1[3];
      offset_1 = info_1[4];
    } else {
      node_1 = info_0[1];
      offset_1 = info_0[2];
      node_0 = info_1[3];
      offset_0 = info_1[4];
    }
    this.lines_range.setStart(node_0, offset_0);
    this.lines_range.setEnd(node_1, offset_1);
    window.getSelection()?.removeAllRanges();
    window.getSelection()?.addRange(this.lines_range);
  }
  release() {
    this.scroll_bar?.release();
    this.lines_range.detach();
    document.getSelection()?.removeRange(this.lines_range)
    document.removeEventListener('pointermove', this.on_lines_pointermove)
    document.removeEventListener('pointerup', this.on_lines_pointerup)
  }
  copy_to_clipboard() {
    return navigator.clipboard.writeText(this.data.code ?? '')
  }
}